const urllib = require('urllib')
const fs = require('fs')
const util = require('./wx/pay/util')
const URLS = {
  micropay: 'https://api.mch.weixin.qq.com/pay/micropay',
  reverse: 'https://api.mch.weixin.qq.com/secapi/pay/reverse',
  unifiedorder: 'https://api.mch.weixin.qq.com/pay/unifiedorder',
  orderquery: 'https://api.mch.weixin.qq.com/pay/orderquery',
  closeorder: 'https://api.mch.weixin.qq.com/pay/closeorder',
  refund: 'https://api.mch.weixin.qq.com/secapi/pay/refund',
  refundquery: 'https://api.mch.weixin.qq.com/pay/refundquery',
  downloadbill: 'https://api.mch.weixin.qq.com/pay/downloadbill',
  transfers: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers',
  gettransferinfo: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo',
  sendredpack: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack',
  sendgroupredpack: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/sendgroupredpack',
  gethbinfo: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/gethbinfo'
}
const replyData = msg => util.buildXML(msg ? {return_code: 'FAIL', return_msg: msg} : {return_code: 'SUCCESS'})
const config = think.config('wxpay')
module.exports = class extends think.Service {
  /**
     *
     * @param {*} appid 公众号ID
     * @param {*} mchid 微信商户号
     * @param {*} partnerKey 微信支付安全密钥(必填, 在微信商户管理界面获取)
     * @param {*} pfx 证书文件路径
     * @param {*} notify_url 支付结果通知回调地址(选填)
     * @param {*} spbill_create_ip P地址(选填) 可以在初始化的时候传入设为默认值, 不传则默认值为127.0.0.1 调用相关API时传入新值则使用新值
     * @param {*} debug
     */
  constructor (conf = {}, debug = false) {
    super()
    conf = Object.assign(config, conf)
    const {appid, mchid, partnerKey, pfx, notify_url, spbill_create_ip} = conf
    if (!appid) throw new Error('appid fail')
    if (!mchid) throw new Error('mchid fail')
    if (!partnerKey) throw new Error('partnerKey fail')
    this.appid = appid
    this.mchid = mchid
    this.partnerKey = partnerKey
    this.pfx = fs.readFileSync(`${think.ROOT_PATH}/${pfx}`)
    this.notify_url = notify_url
    this.spbill_create_ip = spbill_create_ip || '127.0.0.1'
    this.debug = debug
  }

  async _parse (xml, type) {
    if (this.debug) console.log(xml)
    if (type === 'downloadbill') return xml

    const json = await util.parseXML(xml)
    if (json.return_code !== 'SUCCESS') throw new Error(json.return_msg || 'XMLDataError')
    if (type !== 'middleware_refund') {
      if (json.result_code !== 'SUCCESS') throw new Error(json.err_code || 'XMLDataError')
    }
    switch (type) {
      case 'middleware_refund':
        if (json.appid !== this.appid) throw new Error('appid不匹配')
        if (json.mch_id !== this.mchid) throw new Error('mch_id不匹配')
        const key = util.md5(this.partnerKey).toLowerCase()
        const info = util.decrypt(json.req_info, key)
        json.req_info = await util.parseXML(info)
        break
      case 'transfers':
        if (json.mchid !== this.mchid) throw new Error('mchid不匹配')
        break
      case 'sendredpack':
      case 'sendgroupredpack':
        if (json.wxappid !== this.appid) throw new Error('wxappid不匹配')
        if (json.mch_id !== this.mchid) throw new Error('mchid不匹配')
        break
      case 'gethbinfo':
      case 'gettransferinfo':
        if (json.mch_id !== this.mchid) throw new Error('mchid不匹配')
        break
      default:
        if (json.appid !== this.appid) throw new Error('appid不匹配')
        if (json.mch_id !== this.mchid) throw new Error('mch_id不匹配')
        if (json.sign !== this._getSign(json)) throw new Error('sign签名错误')
    }
    return json
  }

  async _parseBill (xml, format = false) {
    if (util.checkXML(xml)) {
      const json = await util.parseXML(xml)
      throw new Error(json.return_msg || 'XMLDataError')
    }
    if (!format) return xml

    const arr = xml.trim().split(/\r?\n/).filter(item => item.trim())
    const total_data = arr.pop().substr(1).split(',`')
    const total_title = arr.pop().split(',')
    const list_title = arr.shift().split(',')
    const list_data = arr.map(item => item.substr(1).split(',`'))
    return {total_title, total_data, list_title, list_data}
  }

  _getSign (params) {
    const str = util.toQueryString(params) + '&key=' + this.partnerKey
    return util.md5(str).toUpperCase()
  }

  async _request (params, {type, needs = [], cert = false} = {}) {
    const miss = needs.filter(key => !key.split('|').some(key => params[key]))
    if (miss.length) throw new Error('missing params: ' + miss.join(', '))

    // 安全签名
    params.sign = this._getSign(params)
    // 创建请求参数
    const pkg = {method: 'POST', dataType: 'text', data: util.buildXML(params)}
    if (cert) {
      pkg.pfx = this.pfx
      pkg.passphrase = this.mchid
    }

    const {status, data} = await urllib.request(URLS[type], pkg)
    if (status !== 200) throw new Error('request fail')
    return this._parse(data, type)
  }

  // Express中间件
  middlewareForExpress (type = 'pay') {
    return async (req, res, next) => {
      res.reply = msg => {
        res.header('Content-Type', 'application/xml; charset=utf-8')
        res.send(replyData(msg))
      }

      try {
        if (typeof req.body !== 'string') throw new Error('XMLDataError')
        req.weixin = await this._parse(req.body, 'middleware_' + type)
      } catch (err) {
        return res.reply('XMLDataError')
      }

      next()
    }
  }

  // Koa中间件
  middleware (type = 'pay') {
    return async (ctx, next) => {
      ctx.reply = msg => {
        ctx.type = 'application/xml; charset=utf-8'
        ctx.body = replyData(msg)
      }

      try {
        if (typeof ctx.request.body !== 'string') throw new Error('XMLDataError')
        ctx.request.weixin = await this._parse(ctx.request.body, 'middleware_' + type)
      } catch (err) {
        return ctx.reply('XMLDataError')
      }

      await next()
    }
  }

  // 获取H5支付参数(自动下单)
  async getPayParams (params) {
    params.trade_type = 'JSAPI'
    const order = await this.unifiedOrder(params)
    return this.getPayParamsByPrepay(order)
  }

  // 获取H5支付参数(通过预支付会话标志)
  getPayParamsByPrepay (params) {
    const pkg = {
      appId: this.appid,
      timeStamp: '' + (Date.now() / 1000 | 0),
      nonceStr: util.generate(),
      package: 'prepay_id=' + params.prepay_id,
      signType: 'MD5'
    }
    pkg.paySign = this._getSign(pkg)
    pkg.timestamp = pkg.timeStamp
    pkg.prepay_id = params.prepay_id
    return pkg
  }

  // 获取APP支付参数(自动下单)
  async getAppParams (params) {
    params.trade_type = 'APP'
    const order = await this.unifiedOrder(params)
    return this.getAppParamsByPrepay(order)
  }

  // 获取APP支付参数(通过预支付会话标志)
  getAppParamsByPrepay (params) {
    const pkg = {
      appid: this.appid,
      partnerid: this.mchid,
      prepayid: params.prepay_id,
      package: 'Sign=WXPay',
      noncestr: util.generate(),
      timestamp: '' + (Date.now() / 1000 | 0)
    }
    pkg.sign = this._getSign(pkg)
    return pkg
  }

  // 刷卡支付
  micropay (params) {
    const pkg = {
      ...params,
      appid: this.appid,
      mch_id: this.mchid,
      nonce_str: util.generate(),
      spbill_create_ip: params.spbill_create_ip || this.spbill_create_ip
    }

    const needs = ['body', 'out_trade_no', 'total_fee', 'auth_code']
    return this._request(pkg, {type: 'micropay', needs})
  }

  // 撤销订单
  reverse (params) {
    const pkg = {
      ...params,
      appid: this.appid,
      mch_id: this.mchid,
      nonce_str: util.generate()
    }

    const needs = ['transaction_id|out_trade_no']
    return this._request(pkg, {type: 'reverse', needs, cert: true})
  }

  /**
   *  统一下单
   * 商户在小程序中先调用该接口在微信支付服务后台生成预支付交易单，返回正确的预支付交易后调起支付。
   * https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1
   */
  unifiedOrder (params) {
    const pkg = {
      ...params,
      appid: this.appid,
      mch_id: this.mchid,
      nonce_str: util.generate(),
      notify_url: params.notify_url || this.notify_url,
      spbill_create_ip: params.spbill_create_ip || this.spbill_create_ip,
      trade_type: params.trade_type || 'JSAPI'
    }
    const needs = ['body', 'out_trade_no', 'total_fee', 'notify_url']
    if (pkg.trade_type === 'JSAPI') {
      needs.push('openid')
    } else if (pkg.trade_type === 'NATIVE') {
      needs.push('product_id')
    }
    return this._request(pkg, {type: 'unifiedorder', needs})
  }

  /**
   * 查询订单
该接口提供所有微信支付订单的查询，商户可以通过查询订单接口主动查询订单状态，完成下一步的业务逻辑。
需要调用查询接口的情况：
◆ 当商户后台、网络、服务器等出现异常，商户系统最终未接收到支付通知；
◆ 调用支付接口后，返回系统错误或未知交易状态情况；
◆ 调用刷卡支付API，返回USERPAYING的状态；
◆ 调用关单或撤销接口API之前，需确认支付状态；
   * @param {*} params
   */
  orderQuery (params) {
    const pkg = {
      ...params,
      appid: this.appid,
      mch_id: this.mchid,
      nonce_str: util.generate()
    }

    const needs = ['transaction_id|out_trade_no']
    return this._request(pkg, {type: 'orderquery', needs})
  }

  /**
   * 关闭订单
以下情况需要调用关单接口：商户订单支付失败需要生成新单号重新发起支付，要对原订单号调用关单，避免重复支付；系统下单后，用户支付超时，系统退出不再受理，避免用户继续，请调用关单接口。
注意：订单生成后不能马上调用关单接口，最短调用时间间隔为5分钟。
err_code
ORDERPAID	订单已支付	订单已支付，不能发起关单	订单已支付，不能发起关单，请当作已支付的正常交易
SYSTEMERROR	系统错误	系统错误	系统异常，请重新调用该API
ORDERCLOSED	订单已关闭	订单已关闭，无法重复关闭	订单已关闭，无需继续调用
SIGNERROR	签名错误	参数签名结果不正确	请检查签名参数和方法是否都符合签名算法要求
REQUIRE_POST_METHOD	请使用post方法	未使用post传递参数 	请检查请求参数是否通过post方法提交
XML_FORMAT_ERROR	XML格式错误	XML格式错误	请检查XML参数格式是否正确
   * @param {*} params
   */
  closeOrder (params) {
    const pkg = {
      ...params,
      appid: this.appid,
      mch_id: this.mchid,
      nonce_str: util.generate()
    }

    const needs = ['out_trade_no']
    return this._request(pkg, {type: 'closeorder', needs})
  }

  // 申请退款
  refund (params) {
    const pkg = {
      ...params,
      appid: this.appid,
      mch_id: this.mchid,
      nonce_str: util.generate(),
      op_user_id: params.op_user_id || this.mchid
    }

    const needs = ['transaction_id|out_trade_no', 'out_refund_no', 'total_fee', 'refund_fee', 'op_user_id']
    return this._request(pkg, {type: 'refund', needs, cert: true})
  }

  // 查询退款
  refundQuery (params) {
    const pkg = {
      ...params,
      appid: this.appid,
      mch_id: this.mchid,
      nonce_str: util.generate()
    }

    const needs = ['transaction_id|out_trade_no|out_refund_no|refund_id']
    return this._request(pkg, {type: 'refundquery', needs})
  }

  // 下载对帐单
  async downloadBill (params, format = false) {
    const pkg = {
      ...params,
      appid: this.appid,
      mch_id: this.mchid,
      nonce_str: util.generate(),
      bill_type: params.bill_type || 'ALL'
    }

    const needs = ['bill_date']
    const xml = await this._request(pkg, {type: 'downloadbill', needs})
    return this._parseBill(xml, format)
  }

  // 企业付款
  transfers (params) {
    const pkg = {
      ...params,
      mch_appid: this.appid,
      mchid: this.mchid,
      nonce_str: util.generate(),
      check_name: params.check_name || 'FORCE_CHECK',
      spbill_create_ip: params.spbill_create_ip || this.spbill_create_ip
    }

    const needs = ['partner_trade_no', 'openid', 'check_name', 'amount', 'desc']
    if (pkg.check_name == 'FORCE_CHECK') needs.push('re_user_name')
    return this._request(pkg, {type: 'transfers', needs, cert: true})
  }

  // 查询企业付款
  transfersQuery (params) {
    const pkg = {
      ...params,
      appid: this.appid,
      mch_id: this.mchid,
      nonce_str: util.generate()
    }

    const needs = ['partner_trade_no']
    return this._request(pkg, {type: 'gettransferinfo', needs, cert: true})
  }

  // 发送普通红包
  sendRedpack (params) {
    const pkg = {
      ...params,
      wxappid: this.appid,
      mch_id: this.mchid,
      nonce_str: util.generate(),
      client_ip: params.client_ip || this.spbill_create_ip,
      mch_billno: params.mch_billno || (params.mch_autono ? this.mchid + util.getFullDate() + params.mch_autono : ''),
      total_num: params.total_num || 1
    }
    delete pkg.mch_autono

    const needs = ['mch_billno', 'send_name', 're_openid', 'total_amount', 'wishing', 'act_name', 'remark']
    if (pkg.total_amount >= 20000) needs.push('scene_id')
    return this._request(pkg, {type: 'sendredpack', needs, cert: true})
  }

  // 发送裂变红包
  sendGroupRedpack (params) {
    const pkg = {
      ...params,
      wxappid: this.appid,
      mch_id: this.mchid,
      nonce_str: util.generate(),
      mch_billno: params.mch_billno || (params.mch_autono ? this.mchid + util.getFullDate() + params.mch_autono : ''),
      total_num: params.total_num || 3,
      amt_type: params.amt_type || 'ALL_RAND'
    }
    delete pkg.mch_autono

    const needs = ['mch_billno', 'send_name', 're_openid', 'total_amount', 'wishing', 'act_name', 'remark']
    if (pkg.total_amount >= 20000) needs.push('scene_id')
    return this._request(pkg, {type: 'sendgroupredpack', needs, cert: true})
  }

  // 查询红包记录
  redpackQuery (params) {
    const pkg = {
      ...params,
      appid: this.appid,
      mch_id: this.mchid,
      nonce_str: util.generate(),
      bill_type: params.bill_type || 'MCHT'
    }

    const needs = ['mch_billno']
    return this._request(pkg, {type: 'gethbinfo', needs, cert: true})
  }
}
